using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace NFreeMarker.Template.Utility
{
    /**
     * <p>A filter that compresses each sequence of consecutive whitespace
     * to a single line break (if the sequence contains a line break) or a
     * single space. In addition, leading and trailing whitespace is
     * completely removed.</p>
     * 
     * <p>Specify the transform parameter <code>single_line = true</code>
     * to always compress to a single space instead of a line break.</p>
     * 
     * <p>The default buffer size can be overridden by specifying a
     * <code>buffer_size</code> transform parameter (in bytes).</p>
     *
     * <p><b>Note:</b> The compress tag is implemented using this filter</p>
     * 
     * <p>Usage:<br />
     * From java:</p>
     * <pre>
     * SimpleHash root = new SimpleHash();
     *
     * root.put( "standardCompress", new freemarker.template.utility.StandardCompress() );
     *
     * ...
     * </pre>
     *
     * <p>From your FreeMarker template:</p>
     * <pre>
     * &lt;transform standardCompress&gt;
     *   &lt;p&gt;This    paragraph will have
     *       extraneous
     *
     * whitespace removed.&lt;/p&gt;
     * &lt;/transform&gt;
     * </pre>
     *
     * <p>Output:</p>
     * <pre>
     * &lt;p&gt;This paragraph will have
     * extraneous
     * whitespace removed.&lt;/p&gt;
     * </pre>
     * 
     * @version $Id: StandardCompress.java,v 1.14 2004/01/06 17:06:43 szegedia Exp $
     */

    public class StandardCompress : ITemplateTransformModel
    {
        private const string BufferSizeKey = "buffer_size";
        private const string SingleLineKey = "single_line";
        private readonly int _defaultBufferSize;

        public static readonly StandardCompress Instance = new StandardCompress();

        public StandardCompress()
            : this(2048)
        {
        }

        /**
         * @param defaultBufferSize the default amount of characters to buffer
         */
        public StandardCompress(int defaultBufferSize)
        {
            _defaultBufferSize = defaultBufferSize;
        }

        public TextWriter GetWriter(TextWriter output, IDictionary<string, ITemplateModel> args)
        {
            int bufferSize = _defaultBufferSize;
            bool singleLine = false;
            if (args != null)
            {
                try
                {
                    ITemplateModel mod;
                    if (args.TryGetValue(BufferSizeKey, out mod))
                    {
                        var num = (ITemplateNumberModel) mod;
                        bufferSize = num.GetAsNumber().IntValue();
                    }
                    else
                    {
                        // TODO: Throw?
                    }
                } catch (InvalidCastException)
                {
                    throw new TemplateModelException("Expecting numerical argument to " + BufferSizeKey);
                }
                try
                {
                    ITemplateModel mod;
                    if (args.TryGetValue(SingleLineKey, out mod))
                    {
                        var flag = (ITemplateBooleanModel) mod;
                        singleLine = flag.GetAsBoolean();
                    }
                } catch (InvalidCastException)
                {
                    throw new TemplateModelException("Expecting boolean argument to " + SingleLineKey);
                }
            }
            return new StandardCompressWriter(output, bufferSize, singleLine);
        }

        private class StandardCompressWriter : TextWriter
        {
            private const int MAX_EOL_LENGTH = 2; // CRLF is two bytes

            private const int AT_BEGINNING = 0;
            private const int SINGLE_LINE = 1;
            private const int INIT = 2;
            private const int SAW_CR = 3;
            private const int LINEBREAK_CR = 4;
            private const int LINEBREAK_CRLF = 5;
            private const int LINEBREAK_LF = 6;

            private readonly TextWriter _out;
            private readonly char[] _buf;
            private readonly bool _singleLine;

            private int _pos = 0;
            private bool _inWhitespace = true;
            private int _lineBreakState = AT_BEGINNING;

            public StandardCompressWriter(TextWriter output, int bufSize, bool singleLine)
            {
                _out = output;
                _singleLine = singleLine;
                _buf = new char[bufSize];
            }

            public override Encoding Encoding
            {
                get { return _out.Encoding; }
            }

            public override void Write(char[] buffer, int index, int count)
            {
                for (;;)
                {
                    // Need to reserve space for the EOL potentially left in the state machine
                    int room = _buf.Length - _pos - MAX_EOL_LENGTH; 
                    if (room >= count)
                    {
                        WriteHelper(buffer, index, count);
                        break;
                    } else if (room <= 0) {
                        FlushInternal();
                    } else {
                        WriteHelper(buffer, index, room);
                        FlushInternal();
                        index += room;
                        count -= room;
                    }
                }
            }

            private void WriteHelper(char[] cbuf, int off, int len)
            {
                int end = off + len;
                for (int i = off; i < end; i++) {
                    char c = cbuf[i];
                    if (char.IsWhiteSpace(c)) {
                        _inWhitespace = true;
                        UpdateLineBreakState(c);
                    } else if (_inWhitespace) {
                        _inWhitespace = false;
                        WriteLineBreakOrSpace();
                        _buf[_pos++] = c;
                    } else {
                        _buf[_pos++] = c;
                    }
                }
            }

            /*
              \r\n    => CRLF
              \r[^\n] => CR
              \r$     => CR
              [^\r]\n => LF
              ^\n     => LF
            */
            private void UpdateLineBreakState(char c)
            {
                switch (_lineBreakState) {
                case INIT:
                    if (c == '\r') {
                        _lineBreakState = SAW_CR;
                    } else if (c == '\n') {
                        _lineBreakState = LINEBREAK_LF;
                    }
                    break;
                case SAW_CR:
                    if (c == '\n') {
                        _lineBreakState = LINEBREAK_CRLF;
                    } else {
                        _lineBreakState = LINEBREAK_CR;
                    }
                    break;
                }
            }

            private void WriteLineBreakOrSpace()
            {
                switch (_lineBreakState) {
                case SAW_CR:
                    // whitespace ended with CR, fall through
                case LINEBREAK_CR:
                    _buf[_pos++] = '\r';
                    break;
                case LINEBREAK_CRLF:
                case LINEBREAK_LF:
                    if (_lineBreakState == LINEBREAK_CRLF)
                    {
                        _buf[_pos++] = '\r';
                    }
                    _buf[_pos++] = '\n';
                    break;
                case AT_BEGINNING:
                    // ignore leading whitespace
                    break;
                case INIT:
                case SINGLE_LINE:
                    _buf[_pos++] = ' ';
                    break;
                }
                _lineBreakState = (_singleLine) ? SINGLE_LINE : INIT;
            }

            private void FlushInternal()
            {
                _out.Write(_buf, 0, _pos);
                _pos = 0;
            }

            public override void Flush()
            {
                FlushInternal();
                _out.Flush();
            }

            public override void Close()
            {
                FlushInternal();
            }
        }
    }
}